package uk.ac.manchester.tewpl.iceiface.client.curriculummap;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;

import uk.ac.manchester.tewpl.iceiface.client.CurriculumMapWindow;
import uk.ac.manchester.tewpl.iceiface.client.Functors.ParamVoidCallback;
import uk.ac.manchester.tewpl.iceiface.client.Functors.VoidCallback;
import uk.ac.manchester.tewpl.iceiface.client.model.KBIndividualModel;
import uk.ac.manchester.tewpl.iceiface.client.model.KBObjectPropertyModel;

import com.extjs.gxt.ui.client.Style.Orientation;
import com.extjs.gxt.ui.client.Style.Scroll;
import com.extjs.gxt.ui.client.event.ButtonEvent;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.event.MenuEvent;
import com.extjs.gxt.ui.client.event.MessageBoxEvent;
import com.extjs.gxt.ui.client.event.SelectionListener;
import com.extjs.gxt.ui.client.util.Margins;
import com.extjs.gxt.ui.client.util.Point;
import com.extjs.gxt.ui.client.widget.Component;
import com.extjs.gxt.ui.client.widget.ContentPanel;
import com.extjs.gxt.ui.client.widget.Dialog;
import com.extjs.gxt.ui.client.widget.Html;
import com.extjs.gxt.ui.client.widget.Info;
import com.extjs.gxt.ui.client.widget.LayoutContainer;
import com.extjs.gxt.ui.client.widget.MessageBox;
import com.extjs.gxt.ui.client.widget.Text;
import com.extjs.gxt.ui.client.widget.Window;
import com.extjs.gxt.ui.client.widget.button.Button;
import com.extjs.gxt.ui.client.widget.layout.MarginData;
import com.extjs.gxt.ui.client.widget.layout.RowData;
import com.extjs.gxt.ui.client.widget.layout.RowLayout;
import com.extjs.gxt.ui.client.widget.menu.Menu;
import com.extjs.gxt.ui.client.widget.menu.MenuItem;
import com.extjs.gxt.ui.client.widget.tips.Tip;
import com.google.gwt.dom.client.ImageElement;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.DoubleClickEvent;
import com.google.gwt.event.dom.client.DoubleClickHandler;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.event.dom.client.MouseOverEvent;
import com.google.gwt.event.dom.client.MouseOverHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Hyperlink;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.widgetideas.graphics.client.Color;
import com.google.gwt.widgetideas.graphics.client.ImageLoader;

/**
 * @author Seb Richards
 */

public class Map extends Composite {
	
	// Widget container and parent window variables
	private AbsolutePanel container;
	private CurriculumMapWindow window;
	
	// Detail panel objects
	private LayoutContainer detailContainer = new LayoutContainer();
	private LayoutContainer airportPanelsContainer = new LayoutContainer();
	private LayoutContainer forwardRoutesContainer = new LayoutContainer();
	private Collection<AirportPanel> airportPanels = new ArrayList<AirportPanel>();
	
	// Load execution pattern variables
	private boolean mapInitStarted = false;
	private boolean mapInitialised = false;
	
	// Canvas variables
	private Canvas canvas;
	private int canvasHeight = 100;
	private int canvasWidth = 100;
	private Object canvasObjectHover;
	
	// Airport variables
	private Collection<Airport> airports = new ArrayList<Airport>();
	//private Airport airportHover;
	private Airport airportSelected;
	private int airportRadius;
	
	// Routes to draw
	private Collection<Route> routes = new ArrayList<Route>(); 
	
	// Airport selection tree
	private ArrayList<Airport> airportSelectionList = new ArrayList<Airport>();
	
	// Filter variables
	private int filterCount = 0;
	private boolean bb = true; 
	private String initialFilterKeyword;
	private Airport initialFilterAirport;
	
	// Island image variables
	private ImageElement [] islandImages;
	
	// Icon image variables
	private ImageElement resetIcon;
	private ImageElement helpIcon;
	private Point resetIconCoords = new Point(0,0);
	private Point helpIconCoords = new Point(0,0);
	
	// Instructions window
	private Dialog instructionsDialog;
	
	/*
	 * DATA - this needs to be externalised
	 */
	
	// Airport data and formatting
	private final Uri [] uris = DATA.uris;
	private final String [] airportLabels = DATA.airportLabels;
	private final Point [] airportOffsets = DATA.airportOffsets;
	private final Point [] airportLabelOffsets = DATA.airportLabelOffsets;

	// Airport colours
	private final Color colorAirport = DATA.colorAirport;
	private final Color colorAirportSelected = DATA.colorAirportSelected;
	private final Color colorAirportDest = DATA.colorAirportDest;
	private final Color colorAirportDestSelected = DATA.colorAirportDestSelected;
	private final Color colorAirportDestEnroute = DATA.colorAirportDestEnroute;
	private final Color colorAirportDisabled = DATA.colorAirportDisabled;

	// Route colours
	private final Color colorRoute = DATA.colorRoute;
	private final Color colorRouteEnroute = DATA.colorRouteEnroute;

	// Island images
	private static final String[] islandUrls = DATA.islandUrls;
	private final Point [] islandOffsets = DATA.islandOffsets;
	private final float [] islandSizeModifiers = DATA.islandSizeModifiers;
	
	/*
	 * Constructor for CurriculumMap
	 */
	public Map(CurriculumMapWindow w) {
		// Store parent window for special interface methods
		window = w;
		
		// Use an AbsolutePanel to contain the Map
		container = new AbsolutePanel();
		
		// Pass container panel back as widget
		initWidget(container);	
	} // new Map
		
	/*
	 * Initialise and populate the map - not in constructor as Map object created on desktop launch,
	 * instead this is invoked on first call to draw()
	 */
	private void initMap() {
		/* 
		 * initMap must be called only ONCE, and as mapInitialized isn't set until
		 * later, this intermediate variable must be used ONCE
		 */
		if (mapInitStarted == false) {
			mapInitStarted = true;
			// Mask parent window
			window.el().mask("Loading...");
			window.setCursor("wait");
			
			// Load airports first
			loadAirport(0, new VoidCallback() {
				public void work() {
					populateMap(); // When all airports loaded, populate map
				}
			});
			
		}
	}
	
	/*
	 * Load airport i from the database
	 */
	private void loadAirport(final int i, final VoidCallback cb) {
		if (i>=0 && i<uris.length) { // Check there are still airports to create
			// Create i'th airport
			new Airport(uris[i].toString(), airportLabels[i], new AsyncCallback<Airport>() {
	
				@Override
				public void onFailure(Throwable e) {
					MessageBox.alert("Error", "Unable to load airport!\n\n" + e.getMessage(), null);
				}
	
				@Override
				public void onSuccess(Airport airport) {
					airport.setLabelOffset(airportLabelOffsets[i]); // Store label offset data
					//airport.setRelativePosition(DATA.airportOffsetsRelative[i]);
					airports.add(airport); // Add to airport arraylist
					loadAirport(i+1, cb); // Load next airport
				}
				
			}); // new Airport
		} else { // When all airports have been loaded
			cb.work(); // Call callback
		}
	}
		
	/*
	 * Populate map with components
	 */
	private void populateMap() {

		// Create canvas
		canvas = new Canvas(canvasWidth, canvasHeight);
		canvas.setStyleName("my-canvas"); // CSS property
		
		// Initialise the details panel
		initDetailContainer();
		
		// Initialise the context menu
		initContextMenu();
		
		// Create instructions window
		initInstructions();
		
	    // Add canvas mouse event handlers
	    canvas.addMouseDownHandler(new MouseDownHandler() {
			@Override
			public void onMouseDown(MouseDownEvent event) {
				//mouseClick(event.getX(), event.getY(), false);
				mouseDown(event);
			}
	    });	    
	    canvas.addMouseMoveHandler(new MouseMoveHandler() {
			@Override
			public void onMouseMove(MouseMoveEvent event) {
				mouseMove(event);
			}
	    });
	    

	    // Load images
		ImageLoader.loadImages(islandUrls, new ImageLoader.CallBack() {
			public void onImagesLoaded(ImageElement[] imageElements) {
				// Initialise ImageElement array
				islandImages = new ImageElement[islandUrls.length];
				
				// Copy each loaded image over to array
				for (int i=0; i<imageElements.length; i++)
					islandImages[i] = imageElements[i];
				
				// Icons urls
				String [] iconUrls = new String [] {DATA.resetIconUrl, DATA.helpIconUrl};
				// Load icons
				ImageLoader.loadImages(iconUrls, new ImageLoader.CallBack() {
					public void onImagesLoaded(ImageElement[] iconElements) {
						resetIcon = iconElements[0];
						helpIcon = iconElements[1];
						
					    // Mark map as initialised and perform initial draw
						mapInitialised = true;
						window.setCursor("default");
						window.el().unmask();
						draw();
					}
				}); // ImageLoader
			}
		}); // ImageLoader
		    
	} // initMap
	
	/*
	 * Reset the map to it's original state
	 */
	private void resetMap() {
		bb=true;
		// Clear information panels
		clearAirportPanels();
		forwardRoutesContainer.removeAll();
		
		// Clear filters
		filterCount = 0;
		for (Airport airport : airports)
			airport.clearFilteredResults();
		
		initialFilterKeyword = null;
		initialFilterAirport = null;
		
		// Clear results grid
		window.clearData();
		
		// Clear airport selection details
		airportSelected = null;
		airportSelectionList.clear();
		routes.clear();
		
		// Show initial routes
		updateForwardRoutes();
		
		draw();
	}

	/*
	 * Method fired by Window.Resize event to trigger resize and redraw of canvas
	 */
	public void draw(int width, int height) {
		if (mapInitialised) { // Only resize canvas if it's been initialised
			canvas.resize(width, height);
		}
		canvasWidth = width;
		canvasHeight = height;
		draw(); // This must be called here, as invoked by parent window
	}
	
	/*
	 * Draw/refresh the CurriculumMap
	 */
	private void draw() {
		if (!mapInitialised) {
		    initMap(); // Initialise the map/airports
		} else {
			// Clear container and canvas
			container.clear();
			canvas.clear();
			
			// Add canvas object to container
			container.add(canvas);
			
			// Calculate airport placement before drawing anything
			calculateAirports();
			
			// Z axis is stacked from bottom to top
			drawIslands();
			drawRoutes();
			drawAirports();
			drawIcons();
		}
		window.layout(); // Ensure window layout is up to date
	}
	
	/*
	 * Calculate the positions of the airports on the canvas
	 * TODO Add proportional resizing code!!
	 */
	private void calculateAirports() {
		
		// Assign calculated values
		int i = 0;
		for (Airport airport : airports) {
			airport.setXY(airportOffsets[i]);
			i++;
		}
		
		// Calculate proportional size of airports
		//airportRadius = (int) ((canvasWidth < canvasHeight ? canvasWidth : canvasHeight) * 0.03);
		airportRadius = 13;

	}
	
	/*
	 * Draw airports on the canvas
	 */
	private void drawAirports() {
		// For each airport
		for (Airport airport : airports) {
			// Select airport color
			Color airportColor = colorAirportDisabled;
			if (airportSelectionList.size() == 0) { // If this is the initial selection
				if (airport.equals(canvasObjectHover))
					airportColor = colorAirportSelected;
				else
					airportColor = colorAirport;
			} else {
				if (checkValidFinalDestination(airport)) { // If valid destination for selection
					if (airport.equals(canvasObjectHover) || airport.equals(airportSelected)) // Hovered or clicked
						airportColor = colorAirportDestSelected;
					else
						airportColor = colorAirportDest;
				} else { // If not a valid destination for selection
					if (airportSelectionList.size() == 1
							&& airport.equals(airportSelectionList.get(0))) { // If first in tree
						airportColor = colorAirportSelected;
					} else if (airport.equals(airportSelectionList.get(airportSelectionList.size() - 1))) { // If last in tree
						airportColor = colorAirportSelected;
					} else {
						// If en route destination
						for (Route route : routes) {
							if (route.getSource().equals(airport)) {
								airportColor = colorAirportDestEnroute;
								break;
							}
						}
					}
				}
			}
			
			// NB if this point is reached, only unused airports remain, hence colorAirportDisabled assignment above
			
			canvas.setFillStyle(airportColor);

			// Draw airport on canvas
			/*int x = (int) (airport.getRelativePosition().x * canvasWidth);
			int y = (int) (airport.getRelativePosition().y * canvasHeight);*/
			canvas.beginPath();
				canvas.arc(airport.getX(), airport.getY() , airportRadius, 0, Math.PI*2, true);
				//canvas.arc(x, y, airportRadius, 0, Math.PI*2, true);
			canvas.fill();
			
			HTML lbl = new HTML(airport.getMapLabel() + " (" + airport.getCount() + ")");
			lbl.setStyleName("canvas-label");
			container.add(lbl, airport.getX() + airport.getLabelOffest().x, airport.getY() + airport.getLabelOffest().y);
		}
	}
	
	/*
	 * Draw island images on the canvas
	 */
	private void drawIslands() {
		
		// For each island
		for (int i=0; i<islandImages.length; i++) {
			// Calculate proportional island size, based on pre-determined scale factors
			int imgW = (int) (islandImages[i].getWidth() * islandSizeModifiers[i]);
			int imgH = (int) (islandImages[i].getHeight() * islandSizeModifiers[i]);
			
			// Draw island on canvas
			//int x = (int) (DATA.islandOffsetsRelative[i].x * canvasWidth);
			//int y = (int) (DATA.islandOffsetsRelative[i].y * canvasHeight);
			//canvas.drawImage(islandImages[i], x - (imgW/2), y - (imgH/2), imgW, imgH);
			
			canvas.drawImage(islandImages[i], islandOffsets[i].x - (imgW/2), islandOffsets[i].y - (imgH/2), imgW, imgH);
		}
	}
	
	/*
	 * Draw icons
	 */
	private void drawIcons() {
		// Calculate positions
		resetIconCoords.y = helpIcon.getHeight() + 10;
		helpIconCoords.y = 5;
		resetIconCoords.x = canvasWidth - resetIcon.getWidth() - 5;
		helpIconCoords.x = canvasWidth - helpIcon.getWidth() - 5;
		
		// Draw icons
		canvas.drawImage(resetIcon, resetIconCoords.x, resetIconCoords.y);
		canvas.drawImage(helpIcon, helpIconCoords.x, helpIconCoords.y);
	}
	
	/*
	 * Verify routes stored in the Routes array, and draw as apt.
	 */
	private void drawRoutes() {		
		if (routes.size() > 0) { // If there are routes
			canvas.setLineWidth((int) (airportRadius / 4));
			// For each route
			for (Route route : routes) {
				// Retrieve the airports from the map
				Airport source = getCachedAirport(route.getSource());
				Airport destination = getCachedAirport(route.getDestination());
				// If destination airport is drawn on the canvas
				if (source != null && destination != null) {
					boolean drawRoute = true;
					// Set color of route: is this airport a final stage destination?
					if (source == airportSelectionList.get(airportSelectionList.size()-1)) {
						if (airportSelectionList.contains(destination)) {
							drawRoute = false;
						} else {
							if (destination.equals(airportSelected))
								canvas.setStrokeStyle(colorAirportDestSelected);
							else
								canvas.setStrokeStyle(colorRoute);
						}
					} else {
						canvas.setStrokeStyle(colorRouteEnroute);
					}
					
					if (drawRoute) {
						// Get distance between points
						double dX = (destination.getX() - source.getX());
						double dY = (destination.getY() - source.getY());
						double L = Math.sqrt(Math.pow(dX, 2) + Math.pow(dY, 2));
						
						// Calculate points of bezier
						Point cp1 = new Point((int)(L/3), (int) -L/5);
						Point cp2 = new Point((int)(2*L/3), (int) -L/5);
						
						// Draw curve
						canvas.saveContext();
							canvas.translate(source.getX(), source.getY()); // Translate 0,0 to source(x,y)
							if (source.getX() > destination.getX()) // Rotate curve accordingly (i.e. trig limits)
								canvas.rotate(Math.asin(-dY/L));
							else
								canvas.rotate(Math.asin(dY/L));
							
							canvas.beginPath();
								canvas.moveTo(0, 0);
								if (source.getX() > destination.getX()) // Draw curve from 0,0 to (-)L, via cp1+2
									canvas.cubicCurveTo(-cp1.x, cp1.y, -cp2.x, cp2.y, (int) -L, 0);
								else
									canvas.cubicCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, (int) L, 0);
							canvas.stroke();
						canvas.restoreContext();
					}

				}
			}
		}
	}
	
	/*
	 * Return airport object from loaded airports, given the URI
	 */
	private Airport getCachedAirport(Uri uri) {
		for (Airport airport : airports) { // For each airport
			if (airport.equals(uri))
				return airport;
		}
		return null;
		/*int index = airports.indexOf(uri);
		if (index >= 0) {
			return airports.get(index);
		} else {
			return null;
		}*/
	}
	
	/*
	 * Return terminal object from loaded terminals, given the URI
	 */
	private Terminal getCachedTerminal(Uri uri) {
		for (Airport airport : airports) { // For each airport
			for (Terminal terminal : airport.getTerminals()) // for each terminal
				if (uri.equals(terminal))
						return terminal;
		}
		
		// Nothing found
		return null;
	}
	
	/*
	 * Return cached node, either Airport or Terminal
	 */
	private Node getCachedNode(Uri uri) {
		
		// Check for airports
		Airport airport = getCachedAirport(uri);
		if (airport != null)
			return airport;
		
		// Check for terminals
		Terminal terminal = getCachedTerminal(uri);
		if (terminal != null)
			return terminal;
		
		// Nothing found
		return null;
	}
	
	/*
	 * Add routes from source airport to 'routes' ArrayList variable
	 */
	private void updateRoutes() {
		if (airportSelectionList.size() > 0) { // If airport selections have been made
			routes.clear(); // Clear existing routes
			for (int i=0; i<airportSelectionList.size(); i++) {
				Airport airport = airportSelectionList.get(i);
				
				// If end of airport selection tree, find forward routes
				if (i == airportSelectionList.size() - 1) { 
					for (Uri dest : airport.getDestinations()) {
						routes.add(new Route(airport.getUri(), dest)); // Add desinations
					}
				} else { // Else rollover current airport as a route step via (i+1)
					routes.add(new Route(airport.getUri(), airportSelectionList.get(i+1).getUri()));
				}
			}
		} else { // If no selections made
			routes = null;
		}		
	}
	
	/*
	 * Context menu events
	 */
	
	private void initContextMenu() {
		// Initialize menu object
		final Menu menu = new Menu();
		
		// Add items
		/*final MenuItem forwardRoutes = new MenuItem();
		forwardRoutes.setText("Show forward routes");
		menu.add(forwardRoutes);*/
		
		// Reset menu item
		final MenuItem resetMenuItem = new MenuItem("Reset Map");
		resetMenuItem.addSelectionListener(new SelectionListener<MenuEvent> () {
			@Override
			public void componentSelected(MenuEvent ce) {
				confirmResetMap();
			}
		});
		
		menu.add(resetMenuItem);
		
/*		// Add BeforeShow listener
		menu.addListener(Events.BeforeShow, new Listener<MenuEvent>() {
			@Override
			public void handleEvent(MenuEvent e) {
				Airport airport = getAirportUnderMouse(window.el().getX(), window.el().getY());
				menu.removeAll();
				menu.add(new MenuItem("x = " + e.getX() + ", y = " + window.el().getY()));
				
				if (airport != null)
					menu.add(new MenuItem(airport.getLabel()));
			}
		});*/
		
		// Set context menu
		//mapPanel.setContextMenu(menu);
	}

	/* Mouse event handlers */
	
	/*
	 * Mouse movement handler
	 */
	private void mouseMove(MouseMoveEvent event) {
		// Check map has been initialised
		if (mapInitialised) {
			
			Object previousHover = canvasObjectHover;
			canvasObjectHover = getObjectAtCoords(event.getX(), event.getY());
			
			if (canvasObjectHover != previousHover) { // If hover has changed
				if (canvasObjectHover != null) {
					if (canvasObjectHover.getClass() == Airport.class) {
						window.setCursor("pointer");
					} else if (canvasObjectHover.getClass() == ImageElement.class) {
						window.setCursor("pointer");
					}
				} else {
					window.setCursor("default");
				}
				
				// Redraw map with change
				draw();
			}
			
		}
	}

	/*
	 * Mouse click handler
	 */
	private void mouseDown(MouseDownEvent event) {
		// Check map has been initialised
		if (mapInitialised) {

			// Get object that was clicked on cavas
			Object clickedObject = getObjectAtCoords(event.getX(), event.getY());
			
			if (clickedObject != null) { // If something was clicked
				if (clickedObject == resetIcon) { // If reset icon
					confirmResetMap();
				} else if (clickedObject == helpIcon) { // If help icon
					instructionsDialog.show();
				} else if (clickedObject.getClass() == Airport.class) { // If was an Airport
					Airport newSelected = (Airport) clickedObject;
					Airport prevSelected = airportSelected;
					
					if (airportSelectionList.size() == 0) { // If initial airport selection
						addAirportToSelectionList(newSelected);
					} else if (checkValidFinalDestination(newSelected)) { // If valid destination
						if (newSelected == prevSelected) { // If double click - crude due to IE not firing DBL_CLICK
							addAirportToSelectionList(newSelected);
						} else {
							airportSelected = newSelected;
							draw(); // explicitly call draw here
						}
					} else if (airportSelectionList.contains(newSelected)) { // If already in tree
						airportSelected = newSelected;
						draw(); // explicitly call draw here
					} else {// if (!checkValidEnroute(newSelected)){ // If not initial or a valid destination, dead airport
						//confirmResetMap();
						resetMap();
						addAirportToSelectionList(newSelected);
					}
					
					// As selectedAirport has changed, ensure grid is showing correct results
					populateDataGrid(newSelected);
				} // if Airport
			} // if != null
		} // if initialised
	}
	
	/*
	 * Add a given airport to the selection list
	 */
	private void addAirportToSelectionList(Airport airport) {
		// Update tree, panel and routes
		airportSelectionList.add(airport);
		addAirportPanel(airport);
		updateRoutes();
		airportSelected = airport;
		draw();
	}
	
	/*
	 * Ask the user for confirmation to reset the map (when clicked in dead-zone
	 */
	private void confirmResetMap() {
		/*MessageBox.confirm("Reset map", "Would you like to reset the map?", new Listener<MessageBoxEvent>() {
			@Override
			public void handleEvent(MessageBoxEvent be) {
				 if (be.getButtonClicked().getItemId().equals(Dialog.YES))
					 resetMap();
			}
		});*/
		resetMap();
	}
	
	/*
	 * Returns the object at the given co-ordinates
	 */
	private Object getObjectAtCoords(int x, int y) {
		// Check through airports
		for (Airport airport : airports) {
			if (airport.getX() - airportRadius <= x && x <= airport.getX() + airportRadius) {
				if (airport.getY() - airportRadius <= y && y <= airport.getY() + airportRadius) {
					return airport;
				} // if
			} // if
		} // for

		// Check through icons
		if (resetIconCoords.x <= x && x <= resetIconCoords.x + resetIcon.getWidth()) {
			if (resetIconCoords.y <= y && y <= resetIconCoords.y + resetIcon.getHeight()) {
				return resetIcon;
			}
		}
		
		if (helpIconCoords.x <= x && x <= helpIconCoords.x + helpIcon.getWidth()) {
			if (helpIconCoords.y <= y && y <= helpIconCoords.y + helpIcon.getHeight()) {
				return helpIcon;
			}
		}
		
		// If nothing found
		return null;
	}
	
	/*
	 * Finds which airport is under the mouse cursor
	 */
	private Airport getAirportAtCoords(int x, int y) {
		// Run through each airport, checking for mouse over
		for (Airport airport : airports) {
			if (airport.getX() - airportRadius <= x && x <= airport.getX() + airportRadius) {
				if (airport.getY() - airportRadius <= y && y <= airport.getY() + airportRadius) {
					return airport;
				} // if
			} // if
		} // for
		return null;
	} // getAirportUnderMouse
	
	/*
	 * Finds which icon is under the mouse cursor
	 */
	private int getIconUnderMouse(int x, int y) {	
		// Check for reset icon
		if (resetIconCoords.x <= x && x <= resetIconCoords.x + resetIcon.getWidth())
			if (resetIconCoords.y <= y && y <= resetIconCoords.y + resetIcon.getHeight())
				return 0;
		
		// Check for help icon
		if (helpIconCoords.x <= x && x <= helpIconCoords.x + helpIcon.getWidth())
			if (helpIconCoords.y <= y && y <= helpIconCoords.y + helpIcon.getHeight())
				return 1;
		
		return -1;
	} // getAirportUnderMouse
	
	/*
	 * Checks if the given airport is a valid destination for the airport
	 * at the bottom of the selection tree
	 */
	private boolean checkValidFinalDestination(Airport airport) {
		if (airportSelectionList.size() >= 0 && !airportSelectionList.contains(airport))
			return airportSelectionList.get(airportSelectionList.size()-1).getDestinations().contains(airport);
		else
			return false;
	}
	
	/*
	 * Checks if the given airport is en-route
	 */
	private boolean checkValidEnroute(Airport airportToFind) {
		for (Airport airport : airportSelectionList)
			if (airport.equals(airportToFind))
				return true;
		return false;
	}
	
	private void initInstructions() {
		// Create and Reset and Instructions buttons
		instructionsDialog = new Dialog();
		instructionsDialog.setHeading("Instructions");  
		instructionsDialog.setButtons(Dialog.CLOSE);
		instructionsDialog.setBlinkModal(true);
		instructionsDialog.setModal(true);
		instructionsDialog.addText("<P>&bull;&nbsp;Click on an &quot;Airport&quot; to see the possible destinations</P>" + 
				"<P>&bull;&nbsp;Select whether you wish to search for &quot;flights&quot; from that Airport or see the Airport Terminals</P>" +
				"<P>&bull;&nbsp;Terminals allow you search for flights for that particular terminal</P>");
		instructionsDialog.setHideOnButtonClick(true);
	}
	
	/* Detail panel methods */
	
	private void initDetailContainer() {
		// Add containers
		detailContainer.add(airportPanelsContainer);
		detailContainer.add(forwardRoutesContainer);//, new RowData(1,0, new Margins(0,0,6,0)));
		
		// Set CSS style
		forwardRoutesContainer.setStyleName("forward-routes-container");
		
		// Show initial routes
		updateForwardRoutes();
	}
	
	/*
	 * Returns the detail container
	 */
	public LayoutContainer getDetailContainer() {
		return detailContainer;
	}
	
	/*
	 * Add airport details panel
	 */
	private void addAirportPanel(final Airport airport) {
		// Initialise panel
		final AirportPanel panel = new AirportPanel(airport, airportPanels.size() + 1);
		panel.setAddFilterCallback(new ParamVoidCallback<Airport>() {
			@Override
			public void work(Airport airport) {
				addFilter(airport);
			}
		});
		airportPanels.add(panel);
		
		// Add panel to GUI and refresh layout
		airportPanelsContainer.add(panel, new RowData(1,0, new Margins(0,0,6,0)));
		
		// Update forward routes
		updateForwardRoutes();
		
		detailContainer.layout();
	}
	
	/*
	 * Forward routes
	 */
	private void updateForwardRoutes() {
		// Clear container
		forwardRoutesContainer.removeAll();
		
		MarginData marginData = new MarginData(6,10,0,6);
		
		// Container for the filter name & number
		LayoutContainer forwardRoutesTitle = new LayoutContainer();
		forwardRoutesTitle.setStyleName("forward-routes-title");
		
		// Add title
		forwardRoutesContainer.add(forwardRoutesTitle, marginData);
		
		Collection<Uri> destinations = new HashSet<Uri>();

		if (airportSelectionList.size() == 0) {
			forwardRoutesTitle.add(new Text("Select an airport:"));
			for (Airport airport : airports)
				destinations.add(airport.getUri());
		} else {
			destinations = airportSelectionList.get(airportSelectionList.size() - 1).getDestinations();
			if (destinations.size() > 0)
				forwardRoutesTitle.add(new Text("Forward Routes:"));
		}
		
		// Sort list alpabetically
		
		if (destinations.size() > 0) {
			// Add forward routes
			for (Uri uri : destinations) {
				final Airport dest = getCachedAirport(uri);
				if (dest != null) {
					// Container for the filter detail
					LayoutContainerMouse forwardRouteContainer = new LayoutContainerMouse();
					String baseStyle = "forward-routes-line-format forward-route";
					forwardRouteContainer.setStyleName(baseStyle);
					
					// Add the forward route
					forwardRouteContainer.add(new Text(dest.getLabel() + " (" + dest.getCount() + ")"));
					
					// Add mouse handlers
					forwardRouteContainer.addMouseHoverStyler(baseStyle, "forward-route-hover");
					forwardRouteContainer.addMouseDownHandler(new MouseDownHandler() {
						@Override
						public void onMouseDown(MouseDownEvent arg0) {
							addAirportToSelectionList(dest);
						}
					});
					
					// Add filter details to filter container
					forwardRoutesContainer.add(forwardRouteContainer, marginData);
				}
			} // for
		} // if
		
	}
	
	/*
	 * Find the AirportPanel for a corresponding airport
	 */
	private AirportPanel getAirportPanel(Airport airport) {
		for (AirportPanel panel : airportPanels) { // For each added panel
			if (panel.equals(airport)) { // Found panel matching 'airport'
				return panel;
			}
		}
		return null;
	}
	
	/*
	 * Remove the AirportPanel for a given Airport from display
	 */
	private void removeAirportPanel(Airport airport) {
		AirportPanel panel = getAirportPanel(airport);
		if (panel != null) {
			airportPanelsContainer.remove(panel);
			airportPanels.remove(panel);
			detailContainer.layout();
		}
	}
	
	/*
	 * Remove all AirportPanels from display
	 */
	private void clearAirportPanels() {
		for (AirportPanel panel : airportPanels)
			airportPanelsContainer.remove(panel);
		airportPanels.clear();
		detailContainer.layout();
	}
	
	/*
	 * Add new filter
	 */
	private void addFilter(final Airport airport) {
		MessageBox.prompt("Keyword Search", "Please enter a search term:", new Listener<MessageBoxEvent>() {
			@Override
			public void handleEvent(MessageBoxEvent ev) {
				if (ev.getButtonClicked().getItemId().equals(Dialog.OK)) {
					if (ev.getValue() != null) {
						// Mask parent window

						Info.display("", "Filter count = " + filterCount + ", filterExists == " + bb);
						
						String filter = ev.getValue();
						
						//if (filterCount == 0) { // If this is the first filter
						if (bb) {
							window.el().mask("Running search...");
							window.setCursor("wait");
							bb = false;
							filterCount = 10;
							// Perform search
							airport.getResultsWithFilter(filter, new AsyncCallback<Collection<FilterResult>>() {
								@Override
								public void onFailure(Throwable err) { // Fail
									MessageBox.alert("Keyword search", "Search failed with error:\n" + err.getMessage(), null);
									// Unmask parent window
									window.el().unmask();
									window.setCursor("default");
								}
								@Override
								public void onSuccess(Collection<FilterResult> filterResults) {
									if (filterResults != null) { // If results
										// Apply filter
										applyFilter(filterResults);
										
										/*
										 * Tell airports filter is applied;
										 * used for count returns
										 */
										
										for (Airport airport : airports)
											airport.setFilterIsApplied();
										
										// Update interface
										filterInterfaceUpdate(airport);
										
										// Message to user
										/*MessageBox.info("Results", "Search completed with " + filterResults.size() +
												" results at " + airport.getLabel(), null);*/
									} else { // If not results
										MessageBox.info("Results", "No results found!", null);
									}
									
									// Unmask parent window
									window.el().unmask();
									window.setCursor("default");
									
									// Redraw map
									//draw();
								} // onSuccess
							}); // getResultsWithFilter
						} else { // If a filter has already been applied
							/*
							 * Run through airport + it's terminals, delete all FilterResults that don't match,
							 * ensure related individuals (property values) are deleted from followed on
							 * airports/terminals.
							 */
							
							Collection<FilterResult> toRemove = new ArrayList<FilterResult>();
							Collection<FilterResult> toRemove2 = new ArrayList<FilterResult>();
							
							if (airport.getFilterResults() != null) {
								for (FilterResult filterResult : airport.getFilterResults()) {
									String label = filterResult.getIndividual().getLabel();
									String description = filterResult.getIndividual().getDescription();
									
									boolean labelMatch = label.matches("(?i).*" + filter + ".*");
									boolean descMatch = description.matches("(?i).*" + filter + ".*");
									
									System.err.println("Seaching: " + label + " with : " + filter  + " == " + labelMatch);
									
									if (!labelMatch && !descMatch) { // If no matches
										// Mark result to be removed
										toRemove.add(filterResult);
									}
								}
								
								for (FilterResult filterResult : toRemove) {
									airport.removeFilterResult(filterResult);
								}
								
							}
							
							if (airport.getTerminals() != null) {
								for (Terminal terminal : airport.getTerminals()) {
									if (terminal.getFilterResults() != null) {
										for (FilterResult filterResult : terminal.getFilterResults()) {
											String label = filterResult.getIndividual().getLabel();
											String description = filterResult.getIndividual().getDescription();
											
											boolean labelMatch = label.matches("(?i).*" + filter + ".*");
											boolean descMatch = description.matches("(?i).*" + filter + ".*");
											
											System.err.println("Seaching: " + label + " with : " + filter  + " == " + labelMatch);
											
											if (!labelMatch && !descMatch) { // If no matches
												// Mark result to be removed
												toRemove2.add(filterResult);
											}
										}
										
										for (FilterResult filterResult : toRemove2) {
											terminal.removeFilterResult(filterResult);
										}
										
									}
								}
							}
							
							Collection<FilterResult> totalRemoved = new ArrayList<FilterResult>();
							totalRemoved.addAll(toRemove);
							totalRemoved.addAll(toRemove2);
							
							// Remove filter results
							for (FilterResult filterResult : totalRemoved) {
								// For each of the sets of related individuals
								for (Collection<KBIndividualModel> propertySets : filterResult.getProperties().values()) {
									// For each related Individual
									for (KBIndividualModel property : propertySets) {
										String uri = property.getClasses().iterator().next().getUri();
										
										// Find class for individual on map
										Node node = getCachedNode(new Uri(uri));
										if (node != null) {
											FilterResult rem = null;
											// Remove item from node's results
											for (FilterResult result : node.getFilterResults()) {
												if (result.getIndividual() == property) {
													rem = result;
													break;
												}
											}
											
											// Remove result from related node
											if (rem != null) {
												node.removeFilterResult(rem);
											}
										}
											
									}
								}
								
							}
								
							airport.addFilterKeyword(filter);
														
							// Update interface
							filterInterfaceUpdate(airport);
							
							// Unmask parent window
							window.el().unmask();
							window.setCursor("default");
							
							
							
						} // if (filterCount = 0), else
						
					} else {
						MessageBox.alert("Keyword Search", "No text was entered!", null);
					} // if (value == null), else
				} // if clicked ok
			} // handleEvent
		}); // MessageBox.prompt
	}
	
	private void filterInterfaceUpdate(Airport airport) {		
		// Update interface
		filterCount += 1;
		
		// Update the AirportPanels
		for (AirportPanel panel : airportPanels)
			panel.updatePanel();
		
		// Update the forward routes
		updateForwardRoutes();
		
		// Show results for this airport in grid
		populateDataGrid(airport);
		
		detailContainer.layout();
		
		draw();
	}
	
	/*
	 * Apply filter
	 */
	private void applyFilter(Collection<FilterResult> filterResults) {
		for (FilterResult result : filterResults){ // For each FilterResult
			// Add individual for this result to airport
			Node node = getCachedNode(new Uri(result.getIndividual().getClasses().iterator().next().getUri()));
			if (node != null)
				node.addFilterResult(result);
			for (KBObjectPropertyModel property : result.getProperties().keySet()) { // For each object property
				for (KBIndividualModel propValue : result.getProperties().get(property)) { // For each property value
					// Get property value's class
					Uri relatedClassUri = new Uri(propValue.getClasses().iterator().next().getUri());
					
					// Get node on map for property value's class
					Node propertyValueNode = getCachedNode(relatedClassUri);
					
					if (propertyValueNode != null) // If related class exists on map
						propertyValueNode.addFilterResult(new FilterResult(propValue, null));
					
				} // for each property value
			} // for each property
		} // for each individual
		
	}
	
	/*
	 * Returns the number of applied filters, used to determine which of
	 * Node.getMemberCount() or Node.getFilterResultsCount() should be used
	 */
	public int getFilterCount() {
		return filterCount;
	}
	
	/*
	 * Adaptor between FilterResult[] and DataStore
	 */
	private void populateDataGrid(Airport airport) {
		// If a filter has been applied and there are results
		if (filterCount > 0 && airport.getCount() > 0) {

			// Adaptor for Grid
			ArrayList<java.util.Map<String, Object>> gridResults = new ArrayList<java.util.Map<String,Object>>();
			//for (Airport airport : airports) { // for each airoprt
				
				if (airport.getFilterResults() != null) {
					// For each individual
					for (FilterResult result : airport.getFilterResults()) {
						HashMap<String, Object> map;
						// Read data values from each individual into map for grid
						map = new HashMap<String, Object>();
						//map.put("airport", airport.getLabel());
						map.put("terminal", "");
						map.put("label", result.getIndividual().getLabel());
						map.put("description", result.getIndividual().getDescription());
						map.put("props", "" + (result.getProperties() == null ? "none" : result.getProperties().toString()));
						gridResults.add(map);
					} // for
				}
				
				// For each terminal
				if (airport.getTerminals() != null) {
					for (Terminal terminal : airport.getTerminals()) {
						// For each individual
						if (terminal.getFilterResults() != null) {
							for (FilterResult result : terminal.getFilterResults()) {
								HashMap<String, Object> map;
								// Read data values from each individual into map for grid
								map = new HashMap<String, Object>();
								map.put("label", result.getIndividual().getLabel());
								map.put("description", result.getIndividual().getDescription());
								map.put("terminal", terminal.getLabel());
								map.put("props", "" + (result.getProperties() == null ? "none" : result.getProperties().toString()));
								gridResults.add(map);
							} // for
						} // if
					} // for
				} // if
			//} // for
			
			window.setData(gridResults);
			
		} else {
			window.clearData();
		}
	} // populateDataGrid
	
	
}
